#! coding:utf-8


class Similarity(object):
    """
    相似度计算基类（所实现方法直接采用集合的Jaccard相似度）
    """

    def __init__(self):
        pass

    def symptom_similarity(self, sym_list1, sym_list2):
        """
        计算辨证（序列）相似度（基于单独辨证的Jaccard相似度）

        :param sym_list1: 辨证序列1
        :param sym_list2: 辨证序列2
        :return: 返回Jaccard相似度值
        """
        if not sym_list1 or not sym_list2:
            return 0.0
        self._check_sequence(sym_list1)
        self._check_sequence(sym_list2)
        return self._jaccard_similary(set(sym_list1), set(sym_list2))

    def prescript_similarity(self, prescript1, prescript2):
        """
        计算药方相似度（基于各药方中药材集合的Jaccard相似度）

        :param prescript1: 药方1
        :param prescript2: 药方2
        :return: 返回Jaccard相似度值
        """
        if not prescript1 or not prescript2:
            return 0.0
        self._check_sequence(prescript1)
        self._check_sequence(prescript2)
        return self._jaccard_similary(set(prescript1), set(prescript2))

    def herb_similarity(self, herb1, herb2, herb_effect_dic):
        """
        计算药物相似度（基于药效集合的Jaccard相似度）

        :param herb1: 药物1
        :param herb2: 药物2
        :param herb_effect_dic: 药物-药效映射表
        :return: 返回Jaccard相似度值
        """
        if not herb1 or not herb2:
            return 0.0
        if herb1 == herb2:
            return 1.0
        effect1 = herb_effect_dic.get(herb1, set())
        effect2 = herb_effect_dic.get(herb2, set())
        return self._jaccard_similary(effect1, effect2)

    @staticmethod
    def _jaccard_similary(set1, set2):
        # 计算集合Jaccard相似度
        if not set1 and not set2:
            return 0.0
        intersect_set = set1.intersection(set2)
        union_set = set1.union(set2)
        return len(intersect_set) / len(union_set)

    @staticmethod
    def _check_sequence(item):
        # 序列对象类型检查
        if not isinstance(item, (list, tuple, set)):
            raise TypeError('Incorrect type, should be a sequence.')

    @staticmethod
    def _single_symptom_similarity(symptom1, symptom2, same_symp_set, similar_symp_set, w_same, w_similar):
        sym_pair = symptom1 + ' ' + symptom2 if symptom1 < symptom2 else symptom2 + ' ' + symptom1
        temp_w = 0.0
        if symptom1 == symptom2:
            temp_w = 1.0
        elif sym_pair in same_symp_set:
            temp_w = w_same
        elif sym_pair in similar_symp_set:
            temp_w = w_similar
        return sym_pair, temp_w


class MaximumPairMatchSimilarity(Similarity):
    """
    相似度计算类，最终相似度取列表项配对相似度最大结果
    """

    def __init__(self, same_sym_set, similar_sym_set, herb_effect_dic, w_same=1.0, w_similar=0.7):
        """
        Initializing...

        :param same_sym_set: 相同辨证集合（辨证对以空格分隔，按升序排列）
        :param similar_sym_set: 相似辨证集合（辨证对以空格分隔，按升序排列）
        :param herb_effect_dic: 药物-药效映射表
        :param w_same: 相同辨证对权重
        :param w_similar: 相似辨证对权重
        """
        self._same_symptom_set = same_sym_set
        self._similar_symptom_set = similar_sym_set
        self._same_weight = w_same
        self._similar_weight = w_similar
        self._herb_effect_dic = herb_effect_dic
        super(MaximumPairMatchSimilarity, self).__init__()

    def symptom_similarity(self, sym_list1, sym_list2):
        """
        计算辨证相似度，基于两辨证列表中配对辨证相似度总和进行计算，取配对总和最大值与最大列表长度的比值作为最终结果

        :param sym_list1: 辨证列表1
        :param sym_list2: 辨证列表2
        :return: 返回相似度值
        """
        if not sym_list1 or not sym_list2:
            return 0.0
        self._check_sequence(sym_list1)
        self._check_sequence(sym_list2)
        mat, len1, len2 = self._gen_symptom_sim_matrix(sym_list1, sym_list2)
        return self._cal_matrix_max_sim(mat, len1, len2)

    def prescript_similarity(self, prescript1, prescript2):
        """
        计算药方相似度，基于两药方列表中配对药物相似度总和进行计算，取配对总和最大值与最大列表长度的比值作为最终结果
        药物相似度采用药物药效集合的Jaccard相似度度量

        :param prescript1: 药方1
        :param prescript2: 药方2
        :return: 返回药方相似度值
        """
        if not prescript1 or not prescript2:
            return 0.0
        self._check_sequence(prescript1)
        self._check_sequence(prescript2)
        mat, len1, len2 = self._gen_herb_sim_matrix(prescript1, prescript2, self._herb_effect_dic)
        return self._cal_matrix_max_sim(mat, len1, len2)

    def _gen_symptom_sim_matrix(self, sym_list1, sym_list2):
        # 生成辨证相似度矩阵
        mat = []
        len1 = len(sym_list1)
        len2 = len(sym_list2)
        if len1 > len2:
            sym_list1, sym_list2 = sym_list2, sym_list1
            len1, len2 = len2, len1
        for sym1 in sym_list1:
            val_row = []
            for sym2 in sym_list2:
                _, temp_sim = self._single_symptom_similarity(sym1, sym2, self._same_symptom_set,
                                                              self._similar_symptom_set, self._same_weight,
                                                              self._similar_weight)
                val_row.append(temp_sim)
            mat.append(val_row)
        return mat, len1, len2

    def _gen_herb_sim_matrix(self, prescript1, prescript2, herb_effect_dic):
        # 生成药物相似度矩阵
        len1 = len(prescript1)
        len2 = len(prescript2)
        if len1 > len2:
            prescript1, prescript2 = prescript2, prescript1
            len1, len2 = len2, len1
        mat = []
        for herb1 in prescript1:
            val_row = []
            for herb2 in prescript2:
                val_row.append(self.herb_similarity(herb1, herb2, herb_effect_dic))
            mat.append(val_row)
        return mat, len1, len2

    @staticmethod
    def _cal_matrix_max_sim(mat, len1, len2):
        res = [0.0]
        column_set = set()
        _backtracking_sum(mat, 0, 0.0, len1, len2, column_set, res)
        return res[0] / len2


class GreedyMaximumMatchSimilarity(Similarity):
    """
    相似度计算类，采用启发式（贪心）方法计算相似度
    """

    def __init__(self, same_sym_set, similar_sym_set, herb_effect_dic, w_same=1.0, w_similar=0.7):
        """
        Initializing...

        :param same_sym_set: 相同辨证集合（辨证对以空格分隔，按升序排列）
        :param similar_sym_set: 相似辨证集合（辨证对以空格分隔，按升序排列）
        :param herb_effect_dic: 药物-药效映射表
        :param w_same: 相同辨证对权重
        :param w_similar: 相似辨证对权重
        """
        self._same_symptom_set = same_sym_set
        self._similar_symptom_set = similar_sym_set
        self._same_weight = w_same
        self._similar_weight = w_similar
        self._herb_effect_dic = herb_effect_dic
        super().__init__()

    def symptom_similarity(self, sym_list1, sym_list2):
        """
        基于贪心匹配计算辨证相似度

        :param sym_list1: 辨证列表1
        :param sym_list2: 辨证列表2
        :return: 返回辨证相似度值
        """
        if not sym_list1 or not sym_list2:
            return 0.0
        self._check_sequence(sym_list1)
        self._check_sequence(sym_list2)
        slist = self._gen_symptom_pair_list(sym_list1, sym_list2)
        if len(sym_list1) < len(sym_list2):
            min_len, max_len = len(sym_list1), len(sym_list2)
        else:
            min_len, max_len = len(sym_list2), len(sym_list1)
        sim_sum = self._choose_greedy(slist, min_len)
        return sim_sum / max_len

    def prescript_similarity(self, prescript1, prescript2):
        """
        基于贪心匹配计算药方相似度

        :param prescript1: 药方1
        :param prescript2: 药方2
        :return: 返回药方相似度值
        """
        if not prescript1 or not prescript2:
            return 0.0
        self._check_sequence(prescript1)
        self._check_sequence(prescript2)
        slist = self._gen_herb_pair_list(prescript1, prescript2)
        if len(prescript1) < len(prescript2):
            min_len, max_len = len(prescript1), len(prescript2)
        else:
            min_len, max_len = len(prescript2), len(prescript1)
        sim_sum = self._choose_greedy(slist, min_len)
        return sim_sum / max_len

    def _gen_symptom_pair_list(self, sym_list1, sym_list2):
        temp_list = []
        for sym1 in sym_list1:
            for sym2 in sym_list2:
                sym_pair, sim = self._single_symptom_similarity(sym1, sym2, self._same_symptom_set,
                                                                self._similar_symptom_set, self._same_weight,
                                                                self._similar_weight)
                temp_list.append((sym_pair, sim))
        # 排序结果先按相似度值降序，再按辨证对名称升序
        return sorted(temp_list, key=lambda data: (-data[1], data[0]))

    def _gen_herb_pair_list(self, prescript1, prescript2):
        temp_list = []
        for herb1 in prescript1:
            for herb2 in prescript2:
                sim = self.herb_similarity(herb1, herb2, self._herb_effect_dic)
                herb_pair = herb1 + herb2 if herb1 < herb2 else herb2 + herb1
                temp_list.append((herb_pair, sim))
        return sorted(temp_list, key=lambda data: (-data[1], data[0]))

    @staticmethod
    def _choose_greedy(sorted_list, min_len):
        res = 0.0
        item_set = set()
        idx = 0
        while len(item_set) < 2 * min_len:
            if sorted_list[idx][1] == 0.0:
                break
            cur_weight = sorted_list[idx][1]
            pairs = sorted_list[idx][0].split()
            if pairs[0] not in item_set and pairs[1] not in item_set:
                res += cur_weight
                item_set.add(pairs[0])
                item_set.add(pairs[1])
            idx += 1
        return res

    def greedy_test(self, slist, min_len):
        slist = sorted(slist, key=lambda data: (-data[1], data[0]))
        return self._choose_greedy(slist, min_len)


def _backtracking_sum(matrix, cur_row, cur_sum, n_row, n_column, column_used, res):
    if cur_row == n_row:
        if cur_sum > res[0]:
            res[0] = cur_sum
    else:
        for column in range(n_column):
            if column not in column_used:
                cur_sum += matrix[cur_row][column]
                column_used.add(column)
                _backtracking_sum(matrix, cur_row + 1, cur_sum, n_row, n_column, column_used, res)
                column_used.remove(column)
                cur_sum -= matrix[cur_row][column]


if __name__ == '__main__':
    # mat = [[1, 2, 3, 4, 5, 6],
    #        [1, 0, 4, 5, 2, 6],
    #        [1, 1, 0, 2, 2, 4]]
    # column_set = set()
    # res = [0]
    # _backtracking_sum(mat, 0, 0, 3, 6, column_set, res)
    # print(res[0])
    tlist = [('A1 B1', 2.0), ('A1 B2', 3.4), ('A1 B3', 3.1), ('A1 B4', 1.0), ('A2 B1', 3.4), ('A2 B2', 0.5),
             ('A2 B3', 1.5), ('A2 B4', 5.5), ('A3 B1', 5.0), ('A3 B2', 1.4), ('A3 B3', 3.1), ('A3 B4', 9.0)]
    gs = GreedyMaximumMatchSimilarity(None, None, None)
    res = gs.greedy_test(tlist, 3)
    print(res)
    print(0)
